"""This module contains utilities to provide a basic network play infrastructure

TODO: late join o game start implementation"""
from twisted.internet.protocol import DatagramProtocol
from twisted.internet import reactor
#from zlib import compress, decompress
import cPickle, pygame, sys
from pygame.locals import *
import game, vessels, resource_manager

def mask_to_list(long, num):
    l = [0]*(num+1)
    for i in xrange(0, num + 1):
        if (long & 1<<i) == 1<<i:
            l[i] = 1
        else:
            l[i] = 0
    return l

def _update_controller(controller, info):
    """Updates the given NetController or NetPlayerController with the given info"""
    controller.n_pos = info[0]
    controller.n_speed = info[1]
    controller.n_heading = info[2]
    controller.n_health = info[3]
    controller.actions = mask_to_list(info[4], 6)
    controller.fresh = 1

class ServerProtocol(DatagramProtocol):
    """DatagramProtocol that handles the networking for a server
    
    Has a collection of clients with their controller, to update their ships in the game
    Handles the connects and disconects events for players
    Will try to send a game state snapshot to each player each game cycle
    
    Usage:
    Instantiate with port and maxplayers, add to a reactor and call 'update' each frame"""
    def __init__(self, port=1337, max_players=12):
        """port and max players"""
        self.port = port
        self.max_players = max_players
        self.clients = {} # hash of ip:controller
        self.updated = {} # hash of ip:timestamp
    
    def startProtocol(self):
        DatagramProtocol.startProtocol(self)
        print '[Server] - Started Listening on port ' + str(self.port)
    
    def datagramReceived(self, datagram, address):
        """Takes note of the messages recieved.
        The messages can be:
        'join:[id:pickled class object]' - that will create a new ship (if the
            sender ip don't has one TODO:identify by another thing to allow
            several players on the same lan play on internet)
        or
        'timestamp:keys' - that will update the keystrokes for the sender's ship
        or
        'part' - that will kil the sender's ship and send him a disconnect message"""
        if datagram[:4] == 'join':
            if len(self.clients) >= self.max_players:
                self.transport.write('m:sorry, maximun number of players hit')
                return
            data = datagram.split(':') # datagram is of the type join:id:{pickled class}
            if address not in self.clients:
                s = cPickle.loads(data[2])(NetController)
                s.id = int(data[1]) # set the same id that has on the client
                self.clients[address] = s.controller
                self.updated[address] = 0
                print '[Server] - Added: ' + str(s) + ' from ' + repr(address)
                game.ships.add(s)
                game.debug(s.__class__.__name__ + str(s.id) + ' joined!')
            
            #send game setup contains every other ship details
            initial_state = {}
            for e in game.ships:
                initial_state[e.id] = e.serialize(True)
            self.transport.write('ok:'+cPickle.dumps(initial_state), address)
        elif datagram == 'part': # kill the sender
            self.updated.pop(address, False)
            controller = self.clients.pop(address, False)
            if controller:
                print 'Player ' + str(controller.ship.id) + ' leaves'
                controller.ship.kill()
            self.transport.write('bye', address)
        else:
            # updates a ship state on the server
            # packet is timestamp:actionmask
            data = datagram.split(':')
            controller = self.clients[address]
            try:
                if long(data[0]) > self.updated[address]:
                    self.updated[address] = long(data[0])
                    controller.actions = mask_to_list(long(data[1]), 6)
                else:
                    print "[Server] - Bad or missed packet: " + repr(datagram)
            except Exception:
                print Exception
                print "[Server] - Bad or missed packet: " + repr(datagram)

    def start(self):
        """starts the protocol"""
        reactor.listenUDP(self.port, self)

    def stop(self):
        """kills the protocol"""
        self.transport.stopListening()
        self.stopProtocol()
        reactor.stop()

    def update(self):
        """updates the server snapshot and sends it to all the clients"""
        # generate the state for this tick
        self._state = {}
        map(self._gen_packet, game.ships)
        self.state = str(pygame.time.get_ticks()) + ':' + cPickle.dumps(self._state)
        # send to the clients
        map(self._send_state, self.clients)
        reactor.iterate()

    def _gen_packet(self, ship):
        self._state[ship.id] = ship.serialize()

    def _send_state(self, address):
        """sends the current game state to the given client address"""
        self.transport.write(self.state, address)

class ClientProtocol(DatagramProtocol):
    """Protocol that listens to the server messages to update the game state
    and sends the player key strokes for the server to update it
    
    Usage: instantiate, assign a player BEFORE calling start and call update
    every turn"""
    def __init__(self, server_ip, player = None, port=1337):
        self.server = server_ip
        self.port = port
        self.inited = False
        self.player = player # player ship
        self._known = {} # hash that maps id:ship controller
        self.last_packet_recieved = 0 # time of last reception
        self._stopped = False
    
    def startProtocol(self):
        game.debug('Trying to connect to: ' + self.server + " ...")
        self.transport.connect(self.server, self.port)
        self._known[self.player.id] = self.player.controller
    
    def datagramReceived(self, datagram, address):
        """Does things with the messages from the server.
        The messages can be:
        'm:message' - this packet is a server message (TODO player messages too)
        or
        'ok:pickled hash' - full game state string, contains info for every ship
                            in the game to init them
        or
        'bye' - exit confirmation message
        or
        'timestamp:pickled hash' - normal game message, timestamp followed by a
                                   hash with the state of every ship"""
        if datagram[:2]== 'm:':
            game.debug('[SERVER]: ' + datagram[2:])
        elif datagram[:3] == 'ok:': # we are joining, recieve game string
            if self.inited:
                return
            game.debug("... Connected!")
            data = cPickle.loads(datagram[3:])
            # packet is a hash of {id:[class,pos,speed,heading,health,keys]}
            for e in data:
                if e != self.player.id and e not in self._known:
                    new = data[e]
                    # TODO setup the controller
                    s = new[0](NetController)
                    s.id = e
                    self._known[e] = s.controller
                    _update_controller(s.controller, new[1:])
                    print '[Client] - Added: ' + str(s) + ' from ' + str(address)
                    game.ships.add(s)
                    game.debug(s.__class__.__name__ + str(s.id) + ' joined!')
            self.inited = True
        elif datagram is 'bye':
            self.transport.stopListening()
            self.stopProtocol()
            reactor.stop()
        else:
            try:
                data = datagram.split(':')
                if long(data[0]) < self.last_packet_recieved:
                    print '[Client] - Missed packet: last was ' +\
                          str(self.last_packet_recieved) + ' this is on ' + str(data[0])
                    return
                else:
                    self.last_packet_recieved = long(data[0])
                data = cPickle.loads(data[1])
                # packet is a hash of {id:[timestamp,pos,speed,heading,health,keys]}
                if isinstance(data, dict):
                    for e in data:
                        if e not in self._known: # someone has just joined the game
                            print 'Unknown ship ' + str(e)
                            self.inited = False
                            continue
                        info = data[e]
                        _update_controller(self._known[e], info)
                else:
                    print "[Client] - Bad packet: " + repr(datagram)    
            except Error:
                print "[Client] - Unknown packet: " + repr(datagram)

    def start(self):
        """starts the protocol"""
        reactor.listenUDP(0, self)

    def stop(self):
        """Sends the server a logout signal and waits for confirmation"""
        self._stopped = True
        self.transport.write('part')
        reactor.iterate()
    
    def connectionRefused(self):
        game.debug("server at " + self.server + ":" + str(self.port) + " refused us!!")


    def update(self):
        if not self.inited:
            self.transport.write('join:'+str(self.player.id)\
                            +':'+cPickle.dumps(self.player.__class__))
        self.transport.write(str(pygame.time.get_ticks()) + ':' +\
                             str(self.player.controller.actionmask()))
        reactor.iterate()

class NetController(game.Controller):
    """Controller that relies in messages from a server to manage the ship"""
    _action_map = {'acc':1, 'dec':2, 'left':4, 'right':8, 'prim':16, 'sec':32}
    
    def __init__(self, ship):
        """Inits the ship and all the fields that will come from the server"""
        game.Controller.__init__(self, ship)
        self.fresh = 0
        self.n_heading = 0.
        self.n_speed = 0.
        self.n_health = ship.health
        self.n_pos = ship.position
        self.actions = [0] * 6
        self.methods = [self.ship.accelerate, self.ship.deccelerate,\
                        self.ship.left, self.ship.right, self.ship.fire_prim,\
                        self.ship.fire_sec]

    def control(self, delta):
        if self.fresh:
            s = self.ship
            s.position = [(self.n_pos[0] + s.position[0]) / 2,
                          (self.n_pos[1] + s.position[1]) / 2]
            s.speed = self.n_speed
            if(self.n_heading != s.heading):
                s.heading = self.n_heading
                s._update_sprite()
            s.health = self.n_health
            self.fresh = False
        
        map(self.do_action, self.actions, self.methods)
    
    def do_action(self, do, action):
        """action is a function object due to happen"""
        if do:
            action(game.delta)

class NetPlayerController(NetController):
    def control(self, delta):
        k = pygame.key.get_pressed()
        self.actions = [0] * 6
        if k[K_LEFT]:
            self.actions[2] = 1
        if k[K_RIGHT]:
            self.actions[3] = 1
        if k[K_UP]:
            self.actions[0] = 1
        if k[K_DOWN]:
            self.actions[1] = 1
        if k[K_s]:
            self.actions[5] = 1
        if k[K_d]:
            self.actions[4] = 1
        
        NetController.control(self, delta)